/*

Copyright 2009 Nicola Dal Pont

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Version ${MAJOR_VERSION}.${MINOR_VERSION}${BETA_MARK} Build ${BUILD_NUMBER}

*/

package com.astoolkit.xmlmapping
{
   import flash.events.EventDispatcher;
   import flash.system.ApplicationDomain;
   import flash.utils.Dictionary;
   import flash.utils.describeType;
   import flash.utils.getDefinitionByName;
   import flash.utils.getQualifiedClassName;
   
   import memorphic.xpath.XPathQuery;
   import memorphic.xpath.model.XPathContext;
   
   import mx.collections.ArrayCollection;
   import mx.core.ClassFactory;
   import mx.core.IFactory;
   import mx.core.IMXMLObject;
   import mx.events.PropertyChangeEvent;
   import mx.utils.StringUtil;
   
   [DefaultProperty("mappings")]
   [Event(name="initMapping", type="com.astoolkit.xmlmapping.XmlMappingEvent")]
   public class XmlMapping 
      extends EventDispatcher 
      implements IMXMLObject
   {
      /**
      * keeps a cache of DescribedType objects for performance reasons
      */
      private static var _describedTypesCache : Dictionary;
      private static var _mappingsCache : Dictionary;
      
      
      private static const ERROR_UNDEFINED_TARGET_PROPERTY_CLASS : String = "targetClass must be defined for MapProperty ( targetClass: {0}, targetProperty: {1} )"; 
      private static const ERROR_UNMAPPABLE_MANDATORY_PROPERTY : String = "Property mapping failed ( targetPropertyClass: {0}, targetProperty: {1}, xpath: \"{2}\" )"; 
      private static const ERROR_NULL_TARGET_CLASS : String = "xmlMapping.targetClass cannot be null. Has targetClass getter function been defined for \"{0}\"?"; 
      private static const ERROR_NULL_SOURCE_XML : String = "xml parameter cannot be null: {0}.translate( xml )"; 
      private static const ERROR_TARGET_CLASS_HAS_NO_PUBLIC_PROPERTIES : String = "Target class \"{0}\" has no public properties to map to (1}"; 
      private static const ERROR_CANNOT_MAP_CUSTOM_CLASS_PROPERTY : String = "Cannot map property \"{0}\". mapProperty.mapping \"{1}\" is not a subclass of XmlMapping";
      private static const ERROR_NON_NUMERIC_VALUE_MAPPED_TO_NUMERIC_PROPERTY : String = "Attempt to map a non numeric value to a numeric property ( targetClass: {0}, targetProperty: {1}, value: \"{2}\" )";
      private static const ERROR_PARSER_CLASS_IS_NOT_IXMLMAPPINGPARSER : String = "parserClass is not an IXmlMappingParser ( mappingClass: {0}, targetProperty: {1} )";
      private static const ERROR_INCOMPATIBLE_OVERRIDING_TARGET_CLASS : String = "Incompatible overriding targetClass \"{0}\". Expected sublcass of \"{1}\"";
      private static const ERROR_MULTIPLE_NODES_FOUND_FOR_NON_COLLECTION_TARGET_PROPERTY : String = "Multiple nodes found on xml for non-collection property \"{0}\".";

      public var dispatchEvents : Boolean = false;
      
      private var _targetClass : Class = Object;

      public var mappings : Array;
      
      public var variables : Array;
      
      public var version : String = "*";
      
      private var _xpathContext : XPathContext;
      
      private var _context : XmlMappingContext;
      
      //private var _outObject : Object;
      
      private var _target : Object;
      
      private var _xml : XML;
      
      public function set target( inTarget : Object ) : void
      {
      	if( inTarget == null )
      	{
      		onError(ERROR_NULL_SOURCE_XML, getQualifiedClassName( this ) );
      	}
      	_target = inTarget;
      	_targetClass = getObjectClass( _target );
      }
      
      public function get target() : Object
      {
      	return _target;
      }
      
      public function get context() : XmlMappingContext
      {
         return _context;
      }
      
      public function get xml() : XML
      {
         return _xml;
      }
      
       
      public function translateList( xmlList : XMLList, mappingContext : XmlMappingContext = null) : Array
      {
         if( mappingContext == null )
         {
            mappingContext = new XmlMappingContext();
         }
         _context = mappingContext;
         if( xmlList.length() > 0 ) 
         {
            var outArray : Array = [];
            for each( var child : XML in xmlList )
            {
               outArray.push( translate( child, _context ) );
            }
            return outArray;
         }
         return null;
      }
      
      public function translate( xml : XML, mappingContext : XmlMappingContext = null ) : Object
      {
      	 var outObject : Object;
         if( mappingContext == null )
         {
            mappingContext = new XmlMappingContext();
         }
         _xml = xml;
         _context = mappingContext;
         _context.XMLMAPPING_PRIVATE::init( this );
         dispatchEvent( new XmlMappingEvent( XmlMappingEvent.INIT_MAPPING, mappingContext ) );
         var actualTargetClass : Class = _targetClass;
         if( xml == null )
         {
            onError(ERROR_NULL_SOURCE_XML, getQualifiedClassName( this ) );
         }
         if( !_targetClass )
         {
            onError(ERROR_NULL_TARGET_CLASS, getQualifiedClassName( this ) ); 
         }

         _createXPathContext( xml );
         var classInfo : Dictionary = _registerClass( actualTargetClass, outObject );
         if( _target == null )
         {
         	outObject = new actualTargetClass();
         }
         else
         {
         	outObject = _target;
         }
         var type : DescribedType;
         var mapPropertyEvent : XmlMapPropertyEvent;
         var targetIsVariable : Boolean = false;
         var propertyValue : *;
         for each( var property : MapProperty in _resolveMappingProperties( outObject ) )
         {
            targetIsVariable = property.targetProperty.match( /^\$\w+/ ) != null;
            //map only if property's targetClass is a subclass of this mapping's target class
            if( version != property.version && property.version != "*" )
            {
               continue;
            }
            if( property.targetClass is ClassFactory && 
               !classIs( actualTargetClass, ClassFactory( property.targetClass ).generator ) )
            {
                 continue;
            }
            //--------------------------------------------------------------------------------
            
            //dispatches a XmlMapPropertyEvent event for conditional mapping
            mapPropertyEvent = new XmlMapPropertyEvent( 
               XmlMapPropertyEvent.BEFORE_PROPERTY_MAP,
               _context );
            property.dispatchEvent( mapPropertyEvent );
            if( !mapPropertyEvent.shouldMap )
            {
               continue;
            }
            //--------------------------------------------------------------------------------
            
            if( targetIsVariable )
            {
               type = new DescribedType( ClassFactory( property.targetPropertyClass).generator );
            }
            else
            {
               type = DescribedType( classInfo[ property.targetProperty ] );
            }
            var resolvedNode : Object = resolveNode( xml, property.source );
            if( resolvedNode == null && !property.mandatory )
            {
               continue;
            }
            if( resolvedNode is XMLList && 
               type.classObj != Array &&
               type.classObj != ArrayCollection &&
               XMLList( resolvedNode ).length() > 1 )
            {
               onError( ERROR_MULTIPLE_NODES_FOUND_FOR_NON_COLLECTION_TARGET_PROPERTY, 
                  property.targetProperty );
            }

            if( property.mandatory && resolvedNode == null )
            {
              onError( ERROR_UNMAPPABLE_MANDATORY_PROPERTY, 
                 getQualifiedClassName( type.classObj ),
                 property.targetProperty,
                 property.source
                 );
            }
            
            // Custom parsing ---------------------------------------------------------------------------
            if( property.willTrigger( XmlMapPropertyParseEvent.PARSE ) &&
            property.parserClass != null )
            {
            	//TODO: refine this
            	onError( "You cannot define both a parser class and a parse event handler" );
            }
            if( property.willTrigger( XmlMapPropertyParseEvent.PARSE ) )
            {
            	property.dispatchEvent( new XmlMapPropertyParseEvent( XmlMapPropertyParseEvent.PARSE, 
            		_context,
            		resolvedNode ) );
            		continue;
            }
            if( property.parserClass != null )
            {
               var parser : IXmlMappingParser;
               var classFactory : ClassFactory = ClassFactory( property.parserClass );
               if( !classIs( classFactory.generator, IXmlMappingParser ) )
               {
                  onError( ERROR_PARSER_CLASS_IS_NOT_IXMLMAPPINGPARSER,
                     getQualifiedClassName( this ),
                     property.targetProperty );
               } 
               if( classFactory.generator == prototype.constructor )
               {
                  parser = this as IXmlMappingParser;
               }
               else
               {
                  parser = classFactory.newInstance() as IXmlMappingParser;
               }
               propertyValue = parser.parse( property.targetProperty, outObject, resolvedNode, property.parserContext);
            } // ------------------------------------------------------------------------------------------
            else
            { 
                // Array types 
               if( type.classObj == Array || type.classObj == ArrayCollection )
               {
                  var a : Object = describeType( type.classObj );
                  var collection : ArrayCollection = new ArrayCollection();
                  var collectionObject : Object;
                  for each(var xmlObj : XML in resolveNode( xml, property.source ) )
                  {
                     if( property.mapping == null )
                     {
                        // Base type Array
                        var cName : String = getQualifiedClassName( ClassFactory( property.targetPropertyClass ).generator );
                        var innerType : DescribedType = _createDescribedType( cName );
                        collectionObject = _processProperty( property, innerType, actualTargetClass, xmlObj, outObject );
                     }
                     else 
                     {
                        // Custom type Array
                        collectionObject = _processProperty( property, type, actualTargetClass, xmlObj, outObject );
                     }
                     if( collectionObject is Ignorable )
                     {
                        continue
                     }
                     collection.addItem( collectionObject );
                  }
                  if ( type.classObj == Array )
                  {
                     propertyValue = collection.toArray();
                  }
                  else
                  {
                  	 
                     propertyValue = collection;
                  }
               }
              else
              {
              //Process non array property
                propertyValue = _processProperty( property, type, actualTargetClass, resolvedNode[0] as XML, outObject );
              }
            }
            _context.XMLMAPPING_PRIVATE::mappedProperties[ property ] = propertyValue;
            if( targetIsVariable )
            {
               context.XMLMAPPING_PRIVATE::setVariable( property.targetProperty.substr(1), propertyValue );
            }
            else
            {
               if( outObject[ property.targetProperty ] is ArrayCollection )
               	for each( var item : * in propertyValue  )
               	{
               		ArrayCollection( outObject[ property.targetProperty ] ).addItem( item );
               	}
               else
	               outObject[ property.targetProperty ] = propertyValue;
            }
         }
         _context.XMLMAPPING_PRIVATE::cleanUp();
         return outObject;
      }

      private function _resolveMappingProperties( inOutObject : Object ) : Array
      {
         var outMappings : Array = [];
         for each( var mapping : Object in mappings )
         {
            if( mapping is MapProperty )
            {
               outMappings.push( mapping );
            }
            else if( mapping is MapPropertyGroup )
            {
               outMappings = outMappings.concat( MapPropertyGroup( mapping ).getProperties() );
            } 
         }
         for each( var property : MapProperty in outMappings )
         {
            property.XMLMAPPING_PRIVATE::_parentMapping = this;
            property.addEventListener( 
            	PropertyChangeEvent.PROPERTY_CHANGE, 
            	function( inEvent : PropertyChangeEvent ) : void 
            	{
            		_onPropertyValueChange( inEvent, inOutObject );
            	} );
         }
         return outMappings;
      }
      
      private function _onPropertyValueChange( event : PropertyChangeEvent, inOutObject : Object ) : void
      {
         inOutObject[ MapProperty( event.target ).targetProperty ] = event.newValue;
      }
      

      private function _registerClass( inTargetClass : Class, inOutObject : Object ) : Dictionary
      {
         if( _describedTypesCache == null )
         {
            _describedTypesCache = new Dictionary();
         }
         //DescribedType objects are created analyzing targetClass when targetClass is an user-defined class.
         //When targetClass is Object, they are created using MapProperty objects' information instead.
         var className : String = 
            inTargetClass != Object ?
               getQualifiedClassName( inTargetClass ) :
               getQualifiedClassName( this ); 
         if( _describedTypesCache.hasOwnProperty( className ) )
         {
            return _describedTypesCache[ className ] as Dictionary;
         }
         var properties : Dictionary = new Dictionary();
         if( inTargetClass != Object )
         {
            var propsCount : int;
            var typeXml : XML = describeType( inTargetClass );
            //get description of public variables
            for each( var variable : XML in typeXml.factory.variable )
            {
               properties[ variable.@name.toString() ] = _createDescribedType( variable.@type );
               propsCount ++;
            }
            //get description of public read/write accessors
            for each( var accessor : XML in typeXml.factory.accessor.( 
               @access == "readwrite" || @access == "read" ) )
            {
               properties[ accessor.@name.toString() ] = _createDescribedType( accessor.@type );
               propsCount ++;
            }
            if( propsCount == 0 )
            {   
               onError( ERROR_TARGET_CLASS_HAS_NO_PUBLIC_PROPERTIES,
                  getQualifiedClassName( inTargetClass ),
                  getQualifiedClassName( this ) );
            } 
         }
         else 
         {
            //create descriptors using the information
            //provided in mappings which entries must define "destionationClass"
            for each( var property : MapProperty in _resolveMappingProperties( inOutObject ) )
            {  
               var targetPropertyClass : Class;
               try {
                  targetPropertyClass = ClassFactory( property.targetPropertyClass ).generator;
               }
               catch( e : Error )
               { 
                  onError( ERROR_UNDEFINED_TARGET_PROPERTY_CLASS, 
                      property.targetPropertyClass,
                     property.targetProperty);
               }
              properties[ property.targetProperty ] = _createDescribedType( getQualifiedClassName( targetPropertyClass ) ); 
            }
         }
         _describedTypesCache[ className ] = properties;
         return properties;
      }
      
      
      private function _createDescribedType( typeName : String ) : DescribedType
      {
         var type : DescribedType;
         switch( typeName )
         {
            case "int":
            case "uint":
               type = new DescribedType(
                  null, true, typeName );
               break;
            default:
               type = new DescribedType(
                 Class( getDefinitionByName( typeName ) ) );
            break;
         }
         return type;
      } 
      
      private function _processProperty( property : MapProperty, type : DescribedType, targetClass : Class, xml : XML, inOutObject : Object ) : Object
      {
           if( type.isPrimitive )
           {  
              var val : Number = Number( xml.toString() );
              if( ( type.primitiveType == "int" || type.primitiveType ) == "uint" && isNaN( val ) )
              {
                 if( property.mandatory )
                 {
                    onError( ERROR_NON_NUMERIC_VALUE_MAPPED_TO_NUMERIC_PROPERTY,
                       getQualifiedClassName( targetClass ),
                       property.targetProperty,
                       property.source );
                 }
                 else
                 {
                    return Ignorable.IGNORABLE;
                 }
             }
             switch( type.primitiveType )
             {
                case "int":
                   return int( val.toString() );
                   break;
                case "uint":
                   return uint( val.toString() );
                   break;
             }
           }
           else if( type.classObj == String )
           {
              return xml.toString();
           }
           else if( type.classObj == Boolean )
           {
              return xml.toString() == "true";
           } 
           else if( type.classObj == Number )
           {
              var resolvedNumber : Number = Number( xml.toString() );
              if( isNaN( resolvedNumber ) )
              {
                 if( property.mandatory )
                 {
                    onError( ERROR_NON_NUMERIC_VALUE_MAPPED_TO_NUMERIC_PROPERTY,
                       getQualifiedClassName( targetClass ),
                       property.targetProperty,
                       property.source );
                 }
                 else
                 {
                    return Ignorable.IGNORABLE;
                 }
              }
              return resolvedNumber;
           }
           else if( type.classObj == Date )
           {
              // check Date.parse() documentation for supported formats
              // for non-supported source formats set an IXmlMappingParser for MapProperty.parser
              inOutObject[ property.targetProperty ] = 
                 Date.parse( xml.toString() );
           }
           else
           {
              return _processCustomClassProperty( 
                 property, 
                 xml ); 
           }
         return Ignorable.IGNORABLE;
      }
     
      private function _processCustomClassProperty( property : MapProperty, xml : XML ) : Object
      {
          var mapping : XmlMapping; 
          var mappingClass : Class;
          if( property.mapping is Class )
          {
             if( classIs( Class ( property.mapping ),  XmlMapping ) )
             {
                mappingClass = property.mapping as Class; 
                mapping = new mappingClass() as XmlMapping;
             }
          }
          else if( property.mapping is XmlMapping )
          {
             mapping = property.mapping as XmlMapping;
          }
          else if( property.mapping is String )
          {
             //TODO: catch exception
             mappingClass = Class( getDefinitionByName( property.mapping.toString() ) );
             mapping = new mappingClass() as XmlMapping;
          }
          if( mapping == null )
          {
             onError( ERROR_CANNOT_MAP_CUSTOM_CLASS_PROPERTY,
               property.targetProperty,
               property.mapping == null ? 
                  "null" : 
                  getQualifiedClassName( property.mapping ) );
          }
          return mapping.translate( xml );
      }
      
      public function resolveNode( inXml : XML, inPath : String ) : Object
      {
         var query : XPathQuery = new XPathQuery( inPath, _xpathContext );
         var out : Object = query.exec( inXml );
         if( out.length() == 0 )
         {
            return null;
         }
         return out;
      }
      
      private function _createXPathContext( inXml : XML ) : void
      {
         _xpathContext = new XPathContext();
         for each( var ns : Namespace in inXml.namespaceDeclarations() )
         {
            _xpathContext.namespaces[ ns.prefix ] = ns.uri;
         }
         initXPathContext( _xpathContext );
      }
      
      
      public function initialized(document:Object, id:String):void
      {
      }
      
      public function set targetClass( value : IFactory ) : void
      {
         if( value != null )
         {
           if( !classIs( ClassFactory( value ).generator, _targetClass ) )
           {
              onError( ERROR_INCOMPATIBLE_OVERRIDING_TARGET_CLASS,
                 getQualifiedClassName( ClassFactory( value ).generator ),
                 getQualifiedClassName( _targetClass ) );
           }
            _targetClass = ClassFactory( value ).generator;
         }
      }
     
      /**
      * override this to customize the XPath context
      */
      protected function initXPathContext( inXPathContext : XPathContext ) : void
      {
      }
     
     
      private function onError( message : String,  ... params ) : void
      {
         throw new Error( StringUtil.substitute( message, params ) );
      }

      public static function strictIs( object : Object, rightClass : Class ) : Boolean
      {
         var d : Object =  describeType( object );
         var list : XMLList =  describeType( object ).extendsClass.(@type == getQualifiedClassName( rightClass ) );
         var c1 : Boolean = object is rightClass;
         var c2 : Boolean = list.length() == 0;
         return object is rightClass && list.length() == 0;
      }
     
      public static function classIs( leftClass : Class, rightClass : Class ) : Boolean
      {
         if( leftClass == rightClass )
         {
            return true;
         }
         var list : XMLList =  describeType( leftClass ).factory.extendsClass.(@type == getQualifiedClassName( rightClass ) );
         if( list.length() == 0 )
         {
            list =  describeType( leftClass ).factory.implementsInterface.(@type == getQualifiedClassName( rightClass ) );
         }
         return list.length() > 0;
      }
      
		public static function getObjectClass( inObject : Object ) : Class 
		{
			var outClass : Class;
			var className : String = getQualifiedClassName( inObject );
			var domain : ApplicationDomain = ApplicationDomain.currentDomain;
			
			while( !domain.hasDefinition( className ) )
			{
					if ( domain.parentDomain != null ) {
						domain = domain.parentDomain;
					} else {
						break;
					}
			}
	    	return domain.getDefinition( className ) as Class;
		}
   }
   
}
 
class DescribedType {
   public var classObj : Class;
   public var isPrimitive : Boolean = false;
   public var primitiveType : String;
   public var arrayType : Class = String;
   
   public function DescribedType( 
      classObj : Class,  
      isPrimitive : Boolean = false, 
      primitiveType : String = null
      )
   {
      this.classObj = classObj;
      this.isPrimitive = isPrimitive;
      this.primitiveType = primitiveType;
   }
}

class Ignorable {
   public static const IGNORABLE : Ignorable = new Ignorable();
}